package github

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/go-viper/mapstructure/v2"
	"github.com/google/go-github/v79/github"
	"github.com/google/jsonschema-go/jsonschema"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/shurcooL/githubv4"

	ghErrors "github.com/github/github-mcp-server/pkg/errors"
	"github.com/github/github-mcp-server/pkg/lockdown"
	"github.com/github/github-mcp-server/pkg/sanitize"
	"github.com/github/github-mcp-server/pkg/translations"
	"github.com/github/github-mcp-server/pkg/utils"
)

// PullRequestRead creates a tool to get details of a specific pull request.
func PullRequestRead(getClient GetClientFn, cache *lockdown.RepoAccessCache, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"method": {
				Type: "string",
				Description: `Action to specify what pull request data needs to be retrieved from GitHub. 
Possible options: 
 1. get - Get details of a specific pull request.
 2. get_diff - Get the diff of a pull request.
 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.
 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.
 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.
 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.
`,
				Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments"},
			},
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"pullNumber": {
				Type:        "number",
				Description: "Pull request number",
			},
		},
		Required: []string{"method", "owner", "repo", "pullNumber"},
	}
	WithPagination(schema)

	return mcp.Tool{
			Name:        "pull_request_read",
			Description: t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get details for a single pull request"),
				ReadOnlyHint: true,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			method, err := RequiredParam[string](args, "method")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			pullNumber, err := RequiredInt(args, "pullNumber")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			pagination, err := OptionalPaginationParams(args)
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			client, err := getClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
			}

			switch method {
			case "get":
				result, err := GetPullRequest(ctx, client, cache, owner, repo, pullNumber, flags)
				return result, nil, err
			case "get_diff":
				result, err := GetPullRequestDiff(ctx, client, owner, repo, pullNumber)
				return result, nil, err
			case "get_status":
				result, err := GetPullRequestStatus(ctx, client, owner, repo, pullNumber)
				return result, nil, err
			case "get_files":
				result, err := GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination)
				return result, nil, err
			case "get_review_comments":
				result, err := GetPullRequestReviewComments(ctx, client, cache, owner, repo, pullNumber, pagination, flags)
				return result, nil, err
			case "get_reviews":
				result, err := GetPullRequestReviews(ctx, client, cache, owner, repo, pullNumber, flags)
				return result, nil, err
			case "get_comments":
				result, err := GetIssueComments(ctx, client, cache, owner, repo, pullNumber, pagination, flags)
				return result, nil, err
			default:
				return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
			}
		}
}

func GetPullRequest(ctx context.Context, client *github.Client, cache *lockdown.RepoAccessCache, owner, repo string, pullNumber int, ff FeatureFlags) (*mcp.CallToolResult, error) {
	pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx,
			"failed to get pull request",
			resp,
			err,
		), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil
	}

	// sanitize title/body on response
	if pr != nil {
		if pr.Title != nil {
			pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title))
		}
		if pr.Body != nil {
			pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body))
		}
	}

	if ff.LockdownMode {
		if cache == nil {
			return nil, fmt.Errorf("lockdown cache is not configured")
		}
		login := pr.GetUser().GetLogin()
		if login != "" {
			isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo)
			if err != nil {
				return nil, fmt.Errorf("failed to check content removal: %w", err)
			}

			if !isSafeContent {
				return utils.NewToolResultError("access to pull request is restricted by lockdown mode"), nil
			}
		}
	}

	r, err := json.Marshal(pr)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal response: %w", err)
	}

	return utils.NewToolResultText(string(r)), nil
}

func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) {
	raw, resp, err := client.PullRequests.GetRaw(
		ctx,
		owner,
		repo,
		pullNumber,
		github.RawOptions{Type: github.Diff},
	)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx,
			"failed to get pull request diff",
			resp,
			err,
		), nil
	}

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return utils.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil
	}

	defer func() { _ = resp.Body.Close() }()

	// Return the raw response
	return utils.NewToolResultText(string(raw)), nil
}

func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) {
	pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx,
			"failed to get pull request",
			resp,
			err,
		), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil
	}

	// Get combined status for the head SHA
	status, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, *pr.Head.SHA, nil)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx,
			"failed to get combined status",
			resp,
			err,
		), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return utils.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil
	}

	r, err := json.Marshal(status)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal response: %w", err)
	}

	return utils.NewToolResultText(string(r)), nil
}

func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
	opts := &github.ListOptions{
		PerPage: pagination.PerPage,
		Page:    pagination.Page,
	}
	files, resp, err := client.PullRequests.ListFiles(ctx, owner, repo, pullNumber, opts)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx,
			"failed to get pull request files",
			resp,
			err,
		), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return utils.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil
	}

	r, err := json.Marshal(files)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal response: %w", err)
	}

	return utils.NewToolResultText(string(r)), nil
}

func GetPullRequestReviewComments(ctx context.Context, client *github.Client, cache *lockdown.RepoAccessCache, owner, repo string, pullNumber int, pagination PaginationParams, ff FeatureFlags) (*mcp.CallToolResult, error) {
	opts := &github.PullRequestListCommentsOptions{
		ListOptions: github.ListOptions{
			PerPage: pagination.PerPage,
			Page:    pagination.Page,
		},
	}

	comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx,
			"failed to get pull request review comments",
			resp,
			err,
		), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return utils.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil
	}

	if ff.LockdownMode {
		if cache == nil {
			return nil, fmt.Errorf("lockdown cache is not configured")
		}
		filteredComments := make([]*github.PullRequestComment, 0, len(comments))
		for _, comment := range comments {
			user := comment.GetUser()
			if user == nil {
				continue
			}
			isSafeContent, err := cache.IsSafeContent(ctx, user.GetLogin(), owner, repo)
			if err != nil {
				return utils.NewToolResultError(fmt.Sprintf("failed to check lockdown mode: %v", err)), nil
			}
			if isSafeContent {
				filteredComments = append(filteredComments, comment)
			}
		}
		comments = filteredComments
	}

	r, err := json.Marshal(comments)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal response: %w", err)
	}

	return utils.NewToolResultText(string(r)), nil
}

func GetPullRequestReviews(ctx context.Context, client *github.Client, cache *lockdown.RepoAccessCache, owner, repo string, pullNumber int, ff FeatureFlags) (*mcp.CallToolResult, error) {
	reviews, resp, err := client.PullRequests.ListReviews(ctx, owner, repo, pullNumber, nil)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx,
			"failed to get pull request reviews",
			resp,
			err,
		), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return utils.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil
	}

	if ff.LockdownMode {
		if cache == nil {
			return nil, fmt.Errorf("lockdown cache is not configured")
		}
		filteredReviews := make([]*github.PullRequestReview, 0, len(reviews))
		for _, review := range reviews {
			login := review.GetUser().GetLogin()
			if login != "" {
				isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo)
				if err != nil {
					return nil, fmt.Errorf("failed to check lockdown mode: %w", err)
				}
				if isSafeContent {
					filteredReviews = append(filteredReviews, review)
				}
				reviews = filteredReviews
			}
		}
	}

	r, err := json.Marshal(reviews)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal response: %w", err)
	}

	return utils.NewToolResultText(string(r)), nil
}

// CreatePullRequest creates a tool to create a new pull request.
func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"title": {
				Type:        "string",
				Description: "PR title",
			},
			"body": {
				Type:        "string",
				Description: "PR description",
			},
			"head": {
				Type:        "string",
				Description: "Branch containing changes",
			},
			"base": {
				Type:        "string",
				Description: "Branch to merge into",
			},
			"draft": {
				Type:        "boolean",
				Description: "Create as draft PR",
			},
			"maintainer_can_modify": {
				Type:        "boolean",
				Description: "Allow maintainer edits",
			},
		},
		Required: []string{"owner", "repo", "title", "head", "base"},
	}

	return mcp.Tool{
			Name:        "create_pull_request",
			Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"),
				ReadOnlyHint: false,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			title, err := RequiredParam[string](args, "title")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			head, err := RequiredParam[string](args, "head")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			base, err := RequiredParam[string](args, "base")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			body, err := OptionalParam[string](args, "body")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			draft, err := OptionalParam[bool](args, "draft")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			maintainerCanModify, err := OptionalParam[bool](args, "maintainer_can_modify")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			newPR := &github.NewPullRequest{
				Title: github.Ptr(title),
				Head:  github.Ptr(head),
				Base:  github.Ptr(base),
			}

			if body != "" {
				newPR.Body = github.Ptr(body)
			}

			newPR.Draft = github.Ptr(draft)
			newPR.MaintainerCanModify = github.Ptr(maintainerCanModify)

			client, err := getClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
			}
			pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR)
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to create pull request",
					resp,
					err,
				), nil, nil
			}
			defer func() { _ = resp.Body.Close() }()

			if resp.StatusCode != http.StatusCreated {
				bodyBytes, err := io.ReadAll(resp.Body)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
				}
				return utils.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(bodyBytes))), nil, nil
			}

			// Return minimal response with just essential information
			minimalResponse := MinimalResponse{
				ID:  fmt.Sprintf("%d", pr.GetID()),
				URL: pr.GetHTMLURL(),
			}

			r, err := json.Marshal(minimalResponse)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
			}

			return utils.NewToolResultText(string(r)), nil, nil
		}
}

// UpdatePullRequest creates a tool to update an existing pull request.
func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"pullNumber": {
				Type:        "number",
				Description: "Pull request number to update",
			},
			"title": {
				Type:        "string",
				Description: "New title",
			},
			"body": {
				Type:        "string",
				Description: "New description",
			},
			"state": {
				Type:        "string",
				Description: "New state",
				Enum:        []any{"open", "closed"},
			},
			"draft": {
				Type:        "boolean",
				Description: "Mark pull request as draft (true) or ready for review (false)",
			},
			"base": {
				Type:        "string",
				Description: "New base branch name",
			},
			"maintainer_can_modify": {
				Type:        "boolean",
				Description: "Allow maintainer edits",
			},
			"reviewers": {
				Type:        "array",
				Description: "GitHub usernames to request reviews from",
				Items: &jsonschema.Schema{
					Type: "string",
				},
			},
		},
		Required: []string{"owner", "repo", "pullNumber"},
	}

	return mcp.Tool{
			Name:        "update_pull_request",
			Description: t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_UPDATE_PULL_REQUEST_USER_TITLE", "Edit pull request"),
				ReadOnlyHint: false,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			pullNumber, err := RequiredInt(args, "pullNumber")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			_, draftProvided := args["draft"]
			var draftValue bool
			if draftProvided {
				draftValue, err = OptionalParam[bool](args, "draft")
				if err != nil {
					return utils.NewToolResultError(err.Error()), nil, nil
				}
			}

			update := &github.PullRequest{}
			restUpdateNeeded := false

			if title, ok, err := OptionalParamOK[string](args, "title"); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			} else if ok {
				update.Title = github.Ptr(title)
				restUpdateNeeded = true
			}

			if body, ok, err := OptionalParamOK[string](args, "body"); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			} else if ok {
				update.Body = github.Ptr(body)
				restUpdateNeeded = true
			}

			if state, ok, err := OptionalParamOK[string](args, "state"); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			} else if ok {
				update.State = github.Ptr(state)
				restUpdateNeeded = true
			}

			if base, ok, err := OptionalParamOK[string](args, "base"); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			} else if ok {
				update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)}
				restUpdateNeeded = true
			}

			if maintainerCanModify, ok, err := OptionalParamOK[bool](args, "maintainer_can_modify"); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			} else if ok {
				update.MaintainerCanModify = github.Ptr(maintainerCanModify)
				restUpdateNeeded = true
			}

			// Handle reviewers separately
			reviewers, err := OptionalStringArrayParam(args, "reviewers")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			// If no updates, no draft change, and no reviewers, return error early
			if !restUpdateNeeded && !draftProvided && len(reviewers) == 0 {
				return utils.NewToolResultError("No update parameters provided."), nil, nil
			}

			// Handle REST API updates (title, body, state, base, maintainer_can_modify)
			if restUpdateNeeded {
				client, err := getClient(ctx)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
				}

				_, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update)
				if err != nil {
					return ghErrors.NewGitHubAPIErrorResponse(ctx,
						"failed to update pull request",
						resp,
						err,
					), nil, nil
				}
				defer func() { _ = resp.Body.Close() }()

				if resp.StatusCode != http.StatusOK {
					bodyBytes, err := io.ReadAll(resp.Body)
					if err != nil {
						return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
					}
					return utils.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(bodyBytes))), nil, nil
				}
			}

			// Handle draft status changes using GraphQL
			if draftProvided {
				gqlClient, err := getGQLClient(ctx)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil
				}

				var prQuery struct {
					Repository struct {
						PullRequest struct {
							ID      githubv4.ID
							IsDraft githubv4.Boolean
						} `graphql:"pullRequest(number: $prNum)"`
					} `graphql:"repository(owner: $owner, name: $repo)"`
				}

				err = gqlClient.Query(ctx, &prQuery, map[string]interface{}{
					"owner": githubv4.String(owner),
					"repo":  githubv4.String(repo),
					"prNum": githubv4.Int(pullNumber), // #nosec G115 - pull request numbers are always small positive integers
				})
				if err != nil {
					return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find pull request", err), nil, nil
				}

				currentIsDraft := bool(prQuery.Repository.PullRequest.IsDraft)

				if currentIsDraft != draftValue {
					if draftValue {
						// Convert to draft
						var mutation struct {
							ConvertPullRequestToDraft struct {
								PullRequest struct {
									ID      githubv4.ID
									IsDraft githubv4.Boolean
								}
							} `graphql:"convertPullRequestToDraft(input: $input)"`
						}

						err = gqlClient.Mutate(ctx, &mutation, githubv4.ConvertPullRequestToDraftInput{
							PullRequestID: prQuery.Repository.PullRequest.ID,
						}, nil)
						if err != nil {
							return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to convert pull request to draft", err), nil, nil
						}
					} else {
						// Mark as ready for review
						var mutation struct {
							MarkPullRequestReadyForReview struct {
								PullRequest struct {
									ID      githubv4.ID
									IsDraft githubv4.Boolean
								}
							} `graphql:"markPullRequestReadyForReview(input: $input)"`
						}

						err = gqlClient.Mutate(ctx, &mutation, githubv4.MarkPullRequestReadyForReviewInput{
							PullRequestID: prQuery.Repository.PullRequest.ID,
						}, nil)
						if err != nil {
							return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to mark pull request ready for review", err), nil, nil
						}
					}
				}
			}

			// Handle reviewer requests
			if len(reviewers) > 0 {
				client, err := getClient(ctx)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
				}

				reviewersRequest := github.ReviewersRequest{
					Reviewers: reviewers,
				}

				_, resp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, pullNumber, reviewersRequest)
				if err != nil {
					return ghErrors.NewGitHubAPIErrorResponse(ctx,
						"failed to request reviewers",
						resp,
						err,
					), nil, nil
				}
				defer func() {
					if resp != nil && resp.Body != nil {
						_ = resp.Body.Close()
					}
				}()

				if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
					bodyBytes, err := io.ReadAll(resp.Body)
					if err != nil {
						return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
					}
					return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(bodyBytes))), nil, nil
				}
			}

			// Get the final state of the PR to return
			client, err := getClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
			}

			finalPR, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber)
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx, "Failed to get pull request", resp, err), nil, nil
			}
			defer func() {
				if resp != nil && resp.Body != nil {
					_ = resp.Body.Close()
				}
			}()

			// Return minimal response with just essential information
			minimalResponse := MinimalResponse{
				ID:  fmt.Sprintf("%d", finalPR.GetID()),
				URL: finalPR.GetHTMLURL(),
			}

			r, err := json.Marshal(minimalResponse)
			if err != nil {
				return utils.NewToolResultErrorFromErr("Failed to marshal response", err), nil, nil
			}

			return utils.NewToolResultText(string(r)), nil, nil
		}
}

// ListPullRequests creates a tool to list and filter repository pull requests.
func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"state": {
				Type:        "string",
				Description: "Filter by state",
				Enum:        []any{"open", "closed", "all"},
			},
			"head": {
				Type:        "string",
				Description: "Filter by head user/org and branch",
			},
			"base": {
				Type:        "string",
				Description: "Filter by base branch",
			},
			"sort": {
				Type:        "string",
				Description: "Sort by",
				Enum:        []any{"created", "updated", "popularity", "long-running"},
			},
			"direction": {
				Type:        "string",
				Description: "Sort direction",
				Enum:        []any{"asc", "desc"},
			},
		},
		Required: []string{"owner", "repo"},
	}
	WithPagination(schema)

	return mcp.Tool{
			Name:        "list_pull_requests",
			Description: t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"),
				ReadOnlyHint: true,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			state, err := OptionalParam[string](args, "state")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			head, err := OptionalParam[string](args, "head")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			base, err := OptionalParam[string](args, "base")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			sort, err := OptionalParam[string](args, "sort")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			direction, err := OptionalParam[string](args, "direction")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			pagination, err := OptionalPaginationParams(args)
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			opts := &github.PullRequestListOptions{
				State:     state,
				Head:      head,
				Base:      base,
				Sort:      sort,
				Direction: direction,
				ListOptions: github.ListOptions{
					PerPage: pagination.PerPage,
					Page:    pagination.Page,
				},
			}

			client, err := getClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
			}
			prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts)
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to list pull requests",
					resp,
					err,
				), nil, nil
			}
			defer func() { _ = resp.Body.Close() }()

			if resp.StatusCode != http.StatusOK {
				bodyBytes, err := io.ReadAll(resp.Body)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
				}
				return utils.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(bodyBytes))), nil, nil
			}

			// sanitize title/body on each PR
			for _, pr := range prs {
				if pr == nil {
					continue
				}
				if pr.Title != nil {
					pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title))
				}
				if pr.Body != nil {
					pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body))
				}
			}

			r, err := json.Marshal(prs)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
			}

			return utils.NewToolResultText(string(r)), nil, nil
		}
}

// MergePullRequest creates a tool to merge a pull request.
func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"pullNumber": {
				Type:        "number",
				Description: "Pull request number",
			},
			"commit_title": {
				Type:        "string",
				Description: "Title for merge commit",
			},
			"commit_message": {
				Type:        "string",
				Description: "Extra detail for merge commit",
			},
			"merge_method": {
				Type:        "string",
				Description: "Merge method",
				Enum:        []any{"merge", "squash", "rebase"},
			},
		},
		Required: []string{"owner", "repo", "pullNumber"},
	}

	return mcp.Tool{
			Name:        "merge_pull_request",
			Description: t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"),
				ReadOnlyHint: false,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			pullNumber, err := RequiredInt(args, "pullNumber")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			commitTitle, err := OptionalParam[string](args, "commit_title")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			commitMessage, err := OptionalParam[string](args, "commit_message")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			mergeMethod, err := OptionalParam[string](args, "merge_method")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			options := &github.PullRequestOptions{
				CommitTitle: commitTitle,
				MergeMethod: mergeMethod,
			}

			client, err := getClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
			}
			result, resp, err := client.PullRequests.Merge(ctx, owner, repo, pullNumber, commitMessage, options)
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to merge pull request",
					resp,
					err,
				), nil, nil
			}
			defer func() { _ = resp.Body.Close() }()

			if resp.StatusCode != http.StatusOK {
				bodyBytes, err := io.ReadAll(resp.Body)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
				}
				return utils.NewToolResultError(fmt.Sprintf("failed to merge pull request: %s", string(bodyBytes))), nil, nil
			}

			r, err := json.Marshal(result)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
			}

			return utils.NewToolResultText(string(r)), nil, nil
		}
}

// SearchPullRequests creates a tool to search for pull requests.
func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"query": {
				Type:        "string",
				Description: "Search query using GitHub pull request search syntax",
			},
			"owner": {
				Type:        "string",
				Description: "Optional repository owner. If provided with repo, only pull requests for this repository are listed.",
			},
			"repo": {
				Type:        "string",
				Description: "Optional repository name. If provided with owner, only pull requests for this repository are listed.",
			},
			"sort": {
				Type:        "string",
				Description: "Sort field by number of matches of categories, defaults to best match",
				Enum: []any{
					"comments",
					"reactions",
					"reactions-+1",
					"reactions--1",
					"reactions-smile",
					"reactions-thinking_face",
					"reactions-heart",
					"reactions-tada",
					"interactions",
					"created",
					"updated",
				},
			},
			"order": {
				Type:        "string",
				Description: "Sort order",
				Enum:        []any{"asc", "desc"},
			},
		},
		Required: []string{"query"},
	}
	WithPagination(schema)

	return mcp.Tool{
			Name:        "search_pull_requests",
			Description: t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr"),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"),
				ReadOnlyHint: true,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			result, err := searchHandler(ctx, getClient, args, "pr", "failed to search pull requests")
			return result, nil, err
		}
}

// UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch.
func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"pullNumber": {
				Type:        "number",
				Description: "Pull request number",
			},
			"expectedHeadSha": {
				Type:        "string",
				Description: "The expected SHA of the pull request's HEAD ref",
			},
		},
		Required: []string{"owner", "repo", "pullNumber"},
	}

	return mcp.Tool{
			Name:        "update_pull_request_branch",
			Description: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_UPDATE_PULL_REQUEST_BRANCH_USER_TITLE", "Update pull request branch"),
				ReadOnlyHint: false,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			pullNumber, err := RequiredInt(args, "pullNumber")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			expectedHeadSHA, err := OptionalParam[string](args, "expectedHeadSha")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			opts := &github.PullRequestBranchUpdateOptions{}
			if expectedHeadSHA != "" {
				opts.ExpectedHeadSHA = github.Ptr(expectedHeadSHA)
			}

			client, err := getClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
			}
			result, resp, err := client.PullRequests.UpdateBranch(ctx, owner, repo, pullNumber, opts)
			if err != nil {
				// Check if it's an acceptedError. An acceptedError indicates that the update is in progress,
				// and it's not a real error.
				if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) {
					return utils.NewToolResultText("Pull request branch update is in progress"), nil, nil
				}
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to update pull request branch",
					resp,
					err,
				), nil, nil
			}
			defer func() { _ = resp.Body.Close() }()

			if resp.StatusCode != http.StatusAccepted {
				bodyBytes, err := io.ReadAll(resp.Body)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
				}
				return utils.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(bodyBytes))), nil, nil
			}

			r, err := json.Marshal(result)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
			}

			return utils.NewToolResultText(string(r)), nil, nil
		}
}

type PullRequestReviewWriteParams struct {
	Method     string
	Owner      string
	Repo       string
	PullNumber int32
	Body       string
	Event      string
	CommitID   *string
}

func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			// Either we need the PR GQL Id directly, or we need owner, repo and PR number to look it up.
			// Since our other Pull Request tools are working with the REST Client, will handle the lookup
			// internally for now.
			"method": {
				Type:        "string",
				Description: `The write operation to perform on pull request review.`,
				Enum:        []any{"create", "submit_pending", "delete_pending"},
			},
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"pullNumber": {
				Type:        "number",
				Description: "Pull request number",
			},
			"body": {
				Type:        "string",
				Description: "Review comment text",
			},
			"event": {
				Type:        "string",
				Description: "Review action to perform.",
				Enum:        []any{"APPROVE", "REQUEST_CHANGES", "COMMENT"},
			},
			"commitID": {
				Type:        "string",
				Description: "SHA of commit to review",
			},
		},
		Required: []string{"method", "owner", "repo", "pullNumber"},
	}

	return mcp.Tool{
			Name: "pull_request_review_write",
			Description: t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request.

Available methods:
- create: Create a new review of a pull request. If "event" parameter is provided, the review is submitted. If "event" is omitted, a pending review is created.
- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The "body" and "event" parameters are used when submitting the review.
- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request.
`),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_PULL_REQUEST_REVIEW_WRITE_USER_TITLE", "Write operations (create, submit, delete) on pull request reviews."),
				ReadOnlyHint: false,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			var params PullRequestReviewWriteParams
			if err := mapstructure.Decode(args, &params); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			// Given our owner, repo and PR number, lookup the GQL ID of the PR.
			client, err := getGQLClient(ctx)
			if err != nil {
				return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil, nil
			}

			switch params.Method {
			case "create":
				result, err := CreatePullRequestReview(ctx, client, params)
				return result, nil, err
			case "submit_pending":
				result, err := SubmitPendingPullRequestReview(ctx, client, params)
				return result, nil, err
			case "delete_pending":
				result, err := DeletePendingPullRequestReview(ctx, client, params)
				return result, nil, err
			default:
				return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil
			}
		}
}

func CreatePullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) {
	var getPullRequestQuery struct {
		Repository struct {
			PullRequest struct {
				ID githubv4.ID
			} `graphql:"pullRequest(number: $prNum)"`
		} `graphql:"repository(owner: $owner, name: $repo)"`
	}

	if err := client.Query(ctx, &getPullRequestQuery, map[string]any{
		"owner": githubv4.String(params.Owner),
		"repo":  githubv4.String(params.Repo),
		"prNum": githubv4.Int(params.PullNumber),
	}); err != nil {
		return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
			"failed to get pull request",
			err,
		), nil
	}

	// Now we have the GQL ID, we can create a review
	var addPullRequestReviewMutation struct {
		AddPullRequestReview struct {
			PullRequestReview struct {
				ID githubv4.ID // We don't need this, but a selector is required or GQL complains.
			}
		} `graphql:"addPullRequestReview(input: $input)"`
	}

	addPullRequestReviewInput := githubv4.AddPullRequestReviewInput{
		PullRequestID: getPullRequestQuery.Repository.PullRequest.ID,
		CommitOID:     newGQLStringlikePtr[githubv4.GitObjectID](params.CommitID),
	}

	// Event and Body are provided if we submit a review
	if params.Event != "" {
		addPullRequestReviewInput.Event = newGQLStringlike[githubv4.PullRequestReviewEvent](params.Event)
		addPullRequestReviewInput.Body = githubv4.NewString(githubv4.String(params.Body))
	}

	if err := client.Mutate(
		ctx,
		&addPullRequestReviewMutation,
		addPullRequestReviewInput,
		nil,
	); err != nil {
		return utils.NewToolResultError(err.Error()), nil
	}

	// Return nothing interesting, just indicate success for the time being.
	// In future, we may want to return the review ID, but for the moment, we're not leaking
	// API implementation details to the LLM.
	if params.Event == "" {
		return utils.NewToolResultText("pending pull request created"), nil
	}
	return utils.NewToolResultText("pull request review submitted successfully"), nil
}

func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) {
	// First we'll get the current user
	var getViewerQuery struct {
		Viewer struct {
			Login githubv4.String
		}
	}

	if err := client.Query(ctx, &getViewerQuery, nil); err != nil {
		return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
			"failed to get current user",
			err,
		), nil
	}

	var getLatestReviewForViewerQuery struct {
		Repository struct {
			PullRequest struct {
				Reviews struct {
					Nodes []struct {
						ID    githubv4.ID
						State githubv4.PullRequestReviewState
						URL   githubv4.URI
					}
				} `graphql:"reviews(first: 1, author: $author)"`
			} `graphql:"pullRequest(number: $prNum)"`
		} `graphql:"repository(owner: $owner, name: $name)"`
	}

	vars := map[string]any{
		"author": githubv4.String(getViewerQuery.Viewer.Login),
		"owner":  githubv4.String(params.Owner),
		"name":   githubv4.String(params.Repo),
		"prNum":  githubv4.Int(params.PullNumber),
	}

	if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil {
		return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
			"failed to get latest review for current user",
			err,
		), nil
	}

	// Validate there is one review and the state is pending
	if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 {
		return utils.NewToolResultError("No pending review found for the viewer"), nil
	}

	review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0]
	if review.State != githubv4.PullRequestReviewStatePending {
		errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL)
		return utils.NewToolResultError(errText), nil
	}

	// Prepare the mutation
	var submitPullRequestReviewMutation struct {
		SubmitPullRequestReview struct {
			PullRequestReview struct {
				ID githubv4.ID // We don't need this, but a selector is required or GQL complains.
			}
		} `graphql:"submitPullRequestReview(input: $input)"`
	}

	if err := client.Mutate(
		ctx,
		&submitPullRequestReviewMutation,
		githubv4.SubmitPullRequestReviewInput{
			PullRequestReviewID: &review.ID,
			Event:               githubv4.PullRequestReviewEvent(params.Event),
			Body:                newGQLStringlikePtr[githubv4.String](&params.Body),
		},
		nil,
	); err != nil {
		return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
			"failed to submit pull request review",
			err,
		), nil
	}

	// Return nothing interesting, just indicate success for the time being.
	// In future, we may want to return the review ID, but for the moment, we're not leaking
	// API implementation details to the LLM.
	return utils.NewToolResultText("pending pull request review successfully submitted"), nil
}

func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) {
	// First we'll get the current user
	var getViewerQuery struct {
		Viewer struct {
			Login githubv4.String
		}
	}

	if err := client.Query(ctx, &getViewerQuery, nil); err != nil {
		return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
			"failed to get current user",
			err,
		), nil
	}

	var getLatestReviewForViewerQuery struct {
		Repository struct {
			PullRequest struct {
				Reviews struct {
					Nodes []struct {
						ID    githubv4.ID
						State githubv4.PullRequestReviewState
						URL   githubv4.URI
					}
				} `graphql:"reviews(first: 1, author: $author)"`
			} `graphql:"pullRequest(number: $prNum)"`
		} `graphql:"repository(owner: $owner, name: $name)"`
	}

	vars := map[string]any{
		"author": githubv4.String(getViewerQuery.Viewer.Login),
		"owner":  githubv4.String(params.Owner),
		"name":   githubv4.String(params.Repo),
		"prNum":  githubv4.Int(params.PullNumber),
	}

	if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil {
		return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
			"failed to get latest review for current user",
			err,
		), nil
	}

	// Validate there is one review and the state is pending
	if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 {
		return utils.NewToolResultError("No pending review found for the viewer"), nil
	}

	review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0]
	if review.State != githubv4.PullRequestReviewStatePending {
		errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL)
		return utils.NewToolResultError(errText), nil
	}

	// Prepare the mutation
	var deletePullRequestReviewMutation struct {
		DeletePullRequestReview struct {
			PullRequestReview struct {
				ID githubv4.ID // We don't need this, but a selector is required or GQL complains.
			}
		} `graphql:"deletePullRequestReview(input: $input)"`
	}

	if err := client.Mutate(
		ctx,
		&deletePullRequestReviewMutation,
		githubv4.DeletePullRequestReviewInput{
			PullRequestReviewID: &review.ID,
		},
		nil,
	); err != nil {
		return utils.NewToolResultError(err.Error()), nil
	}

	// Return nothing interesting, just indicate success for the time being.
	// In future, we may want to return the review ID, but for the moment, we're not leaking
	// API implementation details to the LLM.
	return utils.NewToolResultText("pending pull request review successfully deleted"), nil
}

// AddCommentToPendingReview creates a tool to add a comment to a pull request review.
func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			// Ideally, for performance sake this would just accept the pullRequestReviewID. However, we would need to
			// add a new tool to get that ID for clients that aren't in the same context as the original pending review
			// creation. So for now, we'll just accept the owner, repo and pull number and assume this is adding a comment
			// the latest review from a user, since only one can be active at a time. It can later be extended with
			// a pullRequestReviewID parameter if targeting other reviews is desired:
			// mcp.WithString("pullRequestReviewID",
			// 	mcp.Required(),
			// 	mcp.Description("The ID of the pull request review to add a comment to"),
			// ),
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"pullNumber": {
				Type:        "number",
				Description: "Pull request number",
			},
			"path": {
				Type:        "string",
				Description: "The relative path to the file that necessitates a comment",
			},
			"body": {
				Type:        "string",
				Description: "The text of the review comment",
			},
			"subjectType": {
				Type:        "string",
				Description: "The level at which the comment is targeted",
				Enum:        []any{"FILE", "LINE"},
			},
			"line": {
				Type:        "number",
				Description: "The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range",
			},
			"side": {
				Type:        "string",
				Description: "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state",
				Enum:        []any{"LEFT", "RIGHT"},
			},
			"startLine": {
				Type:        "number",
				Description: "For multi-line comments, the first line of the range that the comment applies to",
			},
			"startSide": {
				Type:        "string",
				Description: "For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state",
				Enum:        []any{"LEFT", "RIGHT"},
			},
		},
		Required: []string{"owner", "repo", "pullNumber", "path", "body", "subjectType"},
	}

	return mcp.Tool{
			Name:        "add_comment_to_pending_review",
			Description: t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure)."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_USER_TITLE", "Add review comment to the requester's latest pending pull request review"),
				ReadOnlyHint: false,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			var params struct {
				Owner       string
				Repo        string
				PullNumber  int32
				Path        string
				Body        string
				SubjectType string
				Line        *int32
				Side        *string
				StartLine   *int32
				StartSide   *string
			}
			if err := mapstructure.Decode(args, &params); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			client, err := getGQLClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
			}

			// First we'll get the current user
			var getViewerQuery struct {
				Viewer struct {
					Login githubv4.String
				}
			}

			if err := client.Query(ctx, &getViewerQuery, nil); err != nil {
				return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
					"failed to get current user",
					err,
				), nil, nil
			}

			var getLatestReviewForViewerQuery struct {
				Repository struct {
					PullRequest struct {
						Reviews struct {
							Nodes []struct {
								ID    githubv4.ID
								State githubv4.PullRequestReviewState
								URL   githubv4.URI
							}
						} `graphql:"reviews(first: 1, author: $author)"`
					} `graphql:"pullRequest(number: $prNum)"`
				} `graphql:"repository(owner: $owner, name: $name)"`
			}

			vars := map[string]any{
				"author": githubv4.String(getViewerQuery.Viewer.Login),
				"owner":  githubv4.String(params.Owner),
				"name":   githubv4.String(params.Repo),
				"prNum":  githubv4.Int(params.PullNumber),
			}

			if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil {
				return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
					"failed to get latest review for current user",
					err,
				), nil, nil
			}

			// Validate there is one review and the state is pending
			if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 {
				return utils.NewToolResultError("No pending review found for the viewer"), nil, nil
			}

			review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0]
			if review.State != githubv4.PullRequestReviewStatePending {
				errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL)
				return utils.NewToolResultError(errText), nil, nil
			}

			// Then we can create a new review thread comment on the review.
			var addPullRequestReviewThreadMutation struct {
				AddPullRequestReviewThread struct {
					Thread struct {
						ID githubv4.ID // We don't need this, but a selector is required or GQL complains.
					}
				} `graphql:"addPullRequestReviewThread(input: $input)"`
			}

			if err := client.Mutate(
				ctx,
				&addPullRequestReviewThreadMutation,
				githubv4.AddPullRequestReviewThreadInput{
					Path:                githubv4.String(params.Path),
					Body:                githubv4.String(params.Body),
					SubjectType:         newGQLStringlikePtr[githubv4.PullRequestReviewThreadSubjectType](&params.SubjectType),
					Line:                newGQLIntPtr(params.Line),
					Side:                newGQLStringlikePtr[githubv4.DiffSide](params.Side),
					StartLine:           newGQLIntPtr(params.StartLine),
					StartSide:           newGQLStringlikePtr[githubv4.DiffSide](params.StartSide),
					PullRequestReviewID: &review.ID,
				},
				nil,
			); err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			if addPullRequestReviewThreadMutation.AddPullRequestReviewThread.Thread.ID == nil {
				return utils.NewToolResultError(`Failed to add comment to pending review. Possible reasons:
	- The line number doesn't exist in the pull request diff
	- The file path is incorrect
	- The side (LEFT/RIGHT) is invalid for the specified line
`), nil, nil
			}

			// Return nothing interesting, just indicate success for the time being.
			// In future, we may want to return the review ID, but for the moment, we're not leaking
			// API implementation details to the LLM.
			return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil, nil
		}
}

// RequestCopilotReview creates a tool to request a Copilot review for a pull request.
// Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this
// tool if the configured host does not support it.
func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"owner": {
				Type:        "string",
				Description: "Repository owner",
			},
			"repo": {
				Type:        "string",
				Description: "Repository name",
			},
			"pullNumber": {
				Type:        "number",
				Description: "Pull request number",
			},
		},
		Required: []string{"owner", "repo", "pullNumber"},
	}

	return mcp.Tool{
			Name:        "request_copilot_review",
			Description: t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_REQUEST_COPILOT_REVIEW_USER_TITLE", "Request Copilot review"),
				ReadOnlyHint: false,
			},
			InputSchema: schema,
		},
		func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			pullNumber, err := RequiredInt(args, "pullNumber")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			client, err := getClient(ctx)
			if err != nil {
				return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
			}

			_, resp, err := client.PullRequests.RequestReviewers(
				ctx,
				owner,
				repo,
				pullNumber,
				github.ReviewersRequest{
					// The login name of the copilot reviewer bot
					Reviewers: []string{"copilot-pull-request-reviewer[bot]"},
				},
			)
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to request copilot review",
					resp,
					err,
				), nil, nil
			}
			defer func() { _ = resp.Body.Close() }()

			if resp.StatusCode != http.StatusCreated {
				bodyBytes, err := io.ReadAll(resp.Body)
				if err != nil {
					return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
				}
				return utils.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(bodyBytes))), nil, nil
			}

			// Return nothing on success, as there's not much value in returning the Pull Request itself
			return utils.NewToolResultText(""), nil, nil
		}
}

// newGQLString like takes something that approximates a string (of which there are many types in shurcooL/githubv4)
// and constructs a pointer to it, or nil if the string is empty. This is extremely useful because when we parse
// params from the MCP request, we need to convert them to types that are pointers of type def strings and it's
// not possible to take a pointer of an anonymous value e.g. &githubv4.String("foo").
func newGQLStringlike[T ~string](s string) *T {
	if s == "" {
		return nil
	}
	stringlike := T(s)
	return &stringlike
}

func newGQLStringlikePtr[T ~string](s *string) *T {
	if s == nil {
		return nil
	}
	stringlike := T(*s)
	return &stringlike
}

func newGQLIntPtr(i *int32) *githubv4.Int {
	if i == nil {
		return nil
	}
	gi := githubv4.Int(*i)
	return &gi
}
